package ar.com.fiuba.ElectronicCircuitAnalyst.ui;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.digester.Digester;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.xml.sax.SAXException;

import ar.com.fiuba.ElectronicCircuitAnalyst.application.api.CircuitService;
import ar.com.fiuba.ElectronicCircuitAnalyst.application.api.ComponentService;
import ar.com.fiuba.ElectronicCircuitAnalyst.application.api.SimulationService;
import ar.com.fiuba.ElectronicCircuitAnalyst.application.api.dto.CircuitDto;
import ar.com.fiuba.ElectronicCircuitAnalyst.application.api.dto.ComponentDto;
import ar.com.fiuba.ElectronicCircuitAnalyst.application.api.dto.ConnectionDto;
import ar.com.fiuba.ElectronicCircuitAnalyst.application.api.dto.LeadDto;
import ar.com.fiuba.ElectronicCircuitAnalyst.application.api.dto.SimulationResultDto;
import ar.com.fiuba.ElectronicCircuitAnalyst.application.api.dto.SimulationValuesDto;
import ar.com.fiuba.ElectronicCircuitAnalyst.domain.exceptions.AlreadyExistingCircuitElementException;
import ar.com.fiuba.ElectronicCircuitAnalyst.domain.exceptions.CircuitElementNotFoundException;
import ar.com.fiuba.ElectronicCircuitAnalyst.domain.exceptions.CircuitInputValuesException;
import ar.com.fiuba.ElectronicCircuitAnalyst.domain.exceptions.InvalidCircuitElementException;
import ar.com.fiuba.ElectronicCircuitAnalyst.domain.exceptions.SimulationException;
import ar.com.fiuba.ElectronicCircuitAnalyst.infrastructure.db4o.config.ObjectContainerFactory;

public class Application {

	private ComponentService componentService;

	private ComponentService demultiplexerService;

	private CircuitService circuitService;

	private SimulationService simulationService;

	private BufferedReader input;

	private BufferedWriter output;

	private Digester digester;

	public Application(InputStream input, OutputStream output) {
		this.input = new BufferedReader(new InputStreamReader(input));
		this.output = new BufferedWriter(new OutputStreamWriter(output));
	}

	public void initApp() {
		ApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "ApplicationContext.xml" });
		this.componentService = (ComponentService) context.getBean("componentService");
		this.demultiplexerService = (ComponentService) context.getBean("demultiplexerService");
		this.circuitService = (CircuitService) context.getBean("circuitService");
		this.simulationService = (SimulationService) context.getBean("simulationService");
	}

	public void shutdownApp() {
		ObjectContainerFactory.getdefaultObjectContainer().close();
	}

	/**
	 * Creates a set of CircuitElements with the XMLs contained on the given
	 * folder path. The files on that folder must be named in a determinate way.
	 * Each CircuitElement file must be named with the kind of circuitElement
	 * that it represents, and a number at the end, according to the order in
	 * wich they must be created.
	 * 
	 * @param folderPath
	 */
	public void loadTestSet(String folderPath) {
		File setFolder = new File(folderPath);
		String[] files = setFolder.list();
		if (files == null) {
			writeLine("No hay archivos en el directorio seleccionado");
		} else {
			Arrays.sort(files);
			for (int i = 0; i < files.length; i++) {
				if (isComponent(files[i])) {
					createComponent(this.componentService, getComponentFromXml(folderPath + "/" + files[i]));
				}
			}
			for (int i = 0; i < files.length; i++) {
				if (isDemultiplexor(files[i])) {
					createComponent(this.demultiplexerService, getComponentFromXml(folderPath + "/" + files[i]));
				}
			}
			for (int i = 0; i < files.length; i++) {
				if (isCircuit(files[i])) {
					createCircuit(getCircuitFromXml(folderPath + "/" + files[i]));
				}
			}
		}

	}

	private boolean isCircuit(String fileName) {
		if (fileName.startsWith("circuit") && fileName.endsWith(".xml"))
			return true;
		return false;
	}

	private boolean isDemultiplexor(String fileName) {
		if (fileName.startsWith("Demu") && fileName.endsWith(".xml"))
			return true;
		return false;
	}

	private boolean isComponent(String fileName) {
		if (fileName.startsWith("Comp") && fileName.endsWith(".xml"))
			return true;
		return false;
	}

	/**
	 * Starts the application. Display the initial menu.
	 */
	public void mainMenu() {
		char input;
		do {
			printMainMenu();
			input = readOption();
			switch (input) {
			case '1':
				componentMenu();
				break;
			case '2':
				circuitMenu();
				break;
			case '3':
				simulationMenu();
				break;
			case '4':
				writeLine("Ingrese el directorio donde se ubica el set de pruebas que desea cargar:");
				loadTestSet(readLine());
				break;
			case '5':
				writeLine("Adiós, que tenga un buen día.");
				break;
			default:
				writeLine("\nLa opción ingresada no es válida.");
			}
		} while (input != '5');

	}

	private void componentMenu() {
		char input;
		do {
			printComponentMenu();
			input = readOption();
			switch (input) {
			case '1':
				createComponent();
				break;
			case '2':
				modifyComponent();
				break;
			case '3':
				deleteComponent();
				break;
			case '4':
				createDemultiplexer();
				break;
			case '5':
				modifyDemultiplexer();
				break;
			case '6':
				deleteDemultiplexer();
				break;
			case '7':
				// exit menu
				break;
			default:
				writeLine("\nLa opción ingresada no es válida.");
			}
		} while (input != '7');
	}

	private void circuitMenu() {
		char input;
		do {
			printCircuitMenu();
			input = readOption();
			switch (input) {
			case '1':
				createCircuit(getCircuitFromXml());
				break;
			case '2':
				modifyCircuit();
				break;
			case '3':
				deleteCircuit();
				break;
			case '4':
				// exit menu
				break;
			default:
				writeLine("\nLa opción ingresada no es válida.");
			}
		} while (input != '4');
	}

	private void simulationMenu() {
		char input;
		do {
			printSimulationMenu();
			input = readOption();
			switch (input) {
			case '1':
				simulate();
				break;
			case '2':
				// exit menu
				break;
			default:
				writeLine("\nLa opción ingresada no es válida.");
			}
		} while (input != '2');
	}

	private void printSimulationMenu() {
		writeLine("");
		writeLine("Simulacion de Circuitos - Por favor, elija una opción:");
		writeLine("------------------------------------------------------------");
		writeLine("1 - Iniciar Simulación");
		writeLine("2 - Volver a menú anterior...");
	}

	private void printMainMenu() {
		writeLine("***********************************************************");
		writeLine("* Bienvenido a Circuitronix - Simule hoy, Disfrute mañana *");
		writeLine("***********************************************************");
		writeLine("Menú:");
		writeLine("-----");
		writeLine("1 - Administrar Componentes");
		writeLine("2 - Administrar Circuitos");
		writeLine("3 - Modo Simulación");
		writeLine("4 - Cargar Set de Pruebas");
		writeLine("5 - Salir");
	}

	private void printComponentMenu() {
		writeLine("");
		writeLine("Administración de Componentes - Por favor, elija una opción:");
		writeLine("------------------------------------------------------------");
		writeLine("1 - Crear Componente");
		writeLine("2 - Modificar Componente");
		writeLine("3 - Eliminar Componente");
		writeLine("4 - Crear Demultiplexor");
		writeLine("5 - Modificar Demultiplexor");
		writeLine("6 - Eliminar Demultiplexor");
		writeLine("7 - Volver a menú anterior...");
	}

	private void printCircuitMenu() {
		writeLine("");
		writeLine("Administración de Circuitos - Por favor, elija una opción:");
		writeLine("----------------------------------------------------------");
		writeLine("1 - Crear Circuito");
		writeLine("2 - Modificar Circuito");
		writeLine("3 - Eliminar Circuito");
		writeLine("4 - Volver a menú anterior...");
	}

	/**
	 * Reads a character from the standard input.
	 * 
	 * @return the read character.
	 */
	private char readOption() {
		System.out.print("Ingrese una opcion: ");
		String c = readLine();
		while (c.length() == 0)
			c = readLine();
		return c.charAt(0);
	}

	/**
	 * Creates and initialize a new instance of the Digester. This method must
	 * be called each time before using the Digester.
	 */
	private void initializeXmlParser() {
		this.digester = new Digester();
	}

	/**
	 * Reads a line of text. A line is considered to be terminated by any one of
	 * a line feed ('\n'), a carriage return ('\r'), or a carriage return
	 * followed immediately by a linefeed.
	 * 
	 * @return the read line.
	 */
	private String readLine() {
		try {
			return input.readLine();
		} catch (IOException e) {
			writeLine("No se pudo leer la línea de texto.");
			return "";
		}
	}

	private void writeLine(String string) {
		try {
			output.write(string + "\n");
			output.flush();
		} catch (IOException e) {
			System.out.println("Hay un problema accediendo a la consola.");
		}
	}

	private void simulate() {
		writeLine("Ingrese el nombre del componente que desea simular: ");
		String circuitName = readLine();
		writeLine("Ingrese el path del xml con los datos de la simulacion");
		String xmlPath = readLine();
		SimulationResultDto resultValuesDto = doSimulation(circuitName, xmlPath);
		if (resultValuesDto != null)
			writeLine("Resultado de la simulación \n" + displayResults(resultValuesDto));
		else
			writeLine("No se pudo realizar la simulacion");
	}

	private SimulationResultDto doSimulation(String circuitName, String xmlPath) {
		SimulationResultDto resultValuesDto = null;
		SimulationValuesDto entryValuesDto = getSimulationValuesFromXml(xmlPath);
		if (entryValuesDto != null) {
			try {
				resultValuesDto = this.simulationService.simulate(circuitName, entryValuesDto);
			} catch (CircuitElementNotFoundException e) {
				writeLine(e.getMessage());
			} catch (InvalidCircuitElementException e) {
				writeLine(e.getMessage());
			} catch (CircuitInputValuesException e) {
				writeLine(e.getMessage());
			} catch (SimulationException e) {
				writeLine(e.getMessage());
			}
		} else {
			writeLine("Los datos de entrada no son validos.");
		}
		return resultValuesDto;
	}

	private String displayResults(SimulationResultDto resultValuesDto) {
		String result = "";
		int i = 0;
		for (List<Boolean> resultValues : resultValuesDto.getResults()) {
			result += "Pulse " + i + ": " + convertBooleanListToString(resultValues) + "\n";
			i++;
		}
		return result;
	}

	private String convertBooleanListToString(List<Boolean> pulseValues) {
		String result = "";
		for (Boolean value : pulseValues)
			if (value)
				result += "1 ";
			else
				result += "0 ";
		return result;
	}

	private SimulationValuesDto getSimulationValuesFromXml(String xmlPath) {
		SimulationValuesDto simulationDto = null;
		try {
			Reader xml = new FileReader(new File(xmlPath));
			simulationDto = digestSimulation(xml);
			xml.close();
		} catch (FileNotFoundException e) {
			writeLine("No se encontró ningun archivo con el path indicado. " + e.getMessage());
		} catch (IOException e) {
			writeLine("Hubo un error leyendo el archivo. " + e.getMessage());
		} catch (SAXException e) {
			writeLine("Hay un error en la estructura del archivo. " + e.getMessage());
		}
		return simulationDto;
	}

	private SimulationValuesDto digestSimulation(Reader xml) throws IOException, SAXException {
		SimulationValuesDto simulationValues;
		initializeXmlParser();
		digester.addObjectCreate("simulationValues", SimulationValuesDto.class);
		digester.addCallMethod("simulationValues/pulseValues", "addValues", 1);
		digester.addCallParam("simulationValues/pulseValues", 0);
		simulationValues = (SimulationValuesDto) digester.parse(xml);
		return simulationValues;
	}

	private void createComponent() {
		createComponent(componentService, getComponentFromXml());
	}

	private void createDemultiplexer() {
		createComponent(demultiplexerService, getComponentFromXml());
	}

	/**
	 * Creates a new Component using the given ComponentService instance.
	 * 
	 * @param componentService
	 *            the service used to create the appropiated type of Component.
	 * @param newComponent
	 *            contains data that will be used to create the new Component.
	 */
	private void createComponent(ComponentService componentService, ComponentDto newComponent) {
		if (newComponent != null) {
			try {
				componentService.makeComponent(newComponent);
			} catch (InvalidCircuitElementException e) {
				writeLine("El componente que se intenta crear no es válido:\n\t" + e.getMessage());
			} catch (AlreadyExistingCircuitElementException e) {
				writeLine("El componente que se intenta crear ya existe:\n\t" + e.getMessage());
			}
			writeLine("Componente <" + newComponent.getName() + "> creado exitosamente.");
			writeLine("");
		} else {
			writeLine("No se pudo realizar la operación.");
		}
	}

	/**
	 * 
	 * @return the recovered ComponentDto or null if an error occurs.
	 */
	private ComponentDto getComponentFromXml() {
		writeLine("Ingrese el path del xml con los datos del Componente: ");
		return getComponentFromXml(readLine());
	}

	/**
	 * 
	 * @return the recovered ComponentDto or null if an error occurs.
	 */
	private ComponentDto getComponentFromXml(String xmlPath) {
		ComponentDto componentDto = null;
		try {
			Reader xml = new FileReader(new File(xmlPath));
			componentDto = digestComponent(xml);
			xml.close();
		} catch (FileNotFoundException e) {
			writeLine("No se encontró ningun archivo con el path indicado. " + e.getMessage());
		} catch (IOException e) {
			writeLine("Hubo un error leyendo el archivo. " + e.getMessage());
		} catch (SAXException e) {
			writeLine("Hay un error en la estructura del archivo. " + e.getMessage());
		}
		return componentDto;
	}

	/**
	 * Process a component represented in a xml, mapping it to a ComponentDto
	 * object.
	 * 
	 * @param xml
	 *            : A Reader representing the xml with the component data.
	 * @return The mapped ComponentDto object.
	 * @throws IOException
	 * @throws SAXException
	 */
	private ComponentDto digestComponent(Reader xml) throws IOException, SAXException {
		ComponentDto digestedComponent;
		initializeXmlParser();
		digester.addObjectCreate("component", ComponentDto.class);
		digester.addBeanPropertySetter("component/name", "name");
		digester.addBeanPropertySetter("component/type", "type");
		digester.addBeanPropertySetter("component/inputsCounts", "inputsCounts");
		digester.addCallMethod("component/function", "addFunction", 1);
		digester.addCallParam("component/function", 0);
		digestedComponent = (ComponentDto) digester.parse(xml);
		return digestedComponent;
	}

	private void modifyComponent() {
		modifyComponent(componentService, getComponentFromXml());
	}

	private void modifyDemultiplexer() {
		modifyComponent(demultiplexerService, getComponentFromXml());
	}

	private void modifyComponent(ComponentService componentService, ComponentDto modifiedComponent) {
		if (modifiedComponent != null) {
			if (confirmOperation()) {
				try {
					circuitService.removeRelatedCircuits(modifiedComponent.getName());
					componentService.modifyComponent(modifiedComponent);
				} catch (InvalidCircuitElementException e) {
					writeLine("Hubo un error al intentar modificar el componente:\n\t" + e.getMessage());
				} catch (CircuitElementNotFoundException e) {
					writeLine("El componente que se intenta modificar no se encontró:\n\t" + e.getMessage());
				}
				writeLine("Componente modificado exitosamente.");
				writeLine("");
			}
		} else {
			writeLine("No se pudo realizar la operación.");
		}
	}

	private void deleteComponent() {
		deleteComponent(componentService);
	}

	private void deleteDemultiplexer() {
		deleteComponent(demultiplexerService);
	}

	private void deleteComponent(ComponentService componentService) {
		if (confirmOperation()) {
			System.out.print("Ingrese el nombre del Componente a eliminar: ");
			String name = readLine();
			try {
				circuitService.removeRelatedCircuits(name);
				componentService.removeComponent(name);
				writeLine("El componente ha " + name + " sido eliminado.");
				writeLine("");
			} catch (CircuitElementNotFoundException e) {
				writeLine("No se encontró ningún componente con el nombre indicado:\n\t" + e.getMessage());
			} catch (InvalidCircuitElementException e) {
				writeLine("Hubo un error al intentar eliminar el componente:\n\t" + e.getMessage());
			}
		}
	}

	/**
	 * 
	 * @param newCircuit
	 *            contains data that will be used to create the new Circuit.
	 */
	private void createCircuit(CircuitDto newCircuit) {
		if (newCircuit != null) {
			try {
				circuitService.makeCircuit(newCircuit);
				writeLine("Circuito <" + newCircuit.getName() + "> creado exitosamente.");
				writeLine("");
			} catch (InvalidCircuitElementException e) {
				writeLine("El circuito que se intenta crear no es válido:\n\t" + e.getMessage());
			} catch (AlreadyExistingCircuitElementException e) {
				writeLine("El circuito que se intenta crear ya existe:\n\t" + e.getMessage());
			}
		} else {
			writeLine("No se pudo realizar la operación.");
		}
	}

	/**
	 * 
	 * @return the recovered CircuitDto or null if an error occurs.
	 */
	private CircuitDto getCircuitFromXml() {
		writeLine("Ingrese el path del xml: ");
		return getCircuitFromXml(readLine());
	}

	/**
	 * 
	 * @param xmlPath
	 *            Path to the Xml containing the Circuit data.
	 * @return the recovered CircuitDto or null if an error occurs.
	 */
	private CircuitDto getCircuitFromXml(String xmlPath) {
		CircuitDto circuitDto = null;
		try {
			Reader xml = new FileReader(new File(xmlPath));
			circuitDto = digestCircuit(xml);
			xml.close();
		} catch (FileNotFoundException e) {
			writeLine("No se encontró ningun archivo con el path indicado. " + e.getMessage());
		} catch (IOException e) {
			writeLine("Hubo un error leyendo el archivo. " + e.getMessage());
		} catch (SAXException e) {
			writeLine("Hay un error en la estructura del archivo. " + e.getMessage());
		}
		return circuitDto;
	}

	private void modifyCircuit() {
		CircuitDto modifiedCircuit = getCircuitFromXml();
		if (modifiedCircuit != null) {
			if (confirmOperation()) {
				try {
					circuitService.removeRelatedCircuits(modifiedCircuit.getName());
					circuitService.modifyCircuit(modifiedCircuit);
				} catch (InvalidCircuitElementException e) {
					writeLine("Hubo un error al intentar eliminar el circuito:\n\t" + e.getMessage());
				} catch (CircuitElementNotFoundException e) {
					writeLine("El circuito que se intenta modificar no se encontro:\n\t" + e.getMessage());
				}
				writeLine("El circuito se modificó exitosamente.");
				writeLine("");
			}
		} else {
			writeLine("No se pudo realizar la operación.");
		}
	}

	private CircuitDto digestCircuit(Reader xml) throws IOException, SAXException {
		CircuitDto digestedCircuit;
		initializeXmlParser();
		digester.addObjectCreate("circuit", CircuitDto.class);
		digester.addBeanPropertySetter("circuit/name", "name");
		digester.addBeanPropertySetter("circuit/inputsCount", "inputsCount");
		digester.addBeanPropertySetter("circuit/outputsCount", "outputsCount");
		digester.addCallMethod("circuit/circuitElement", "addComponent", 1);
		digester.addCallParam("circuit/circuitElement", 0);
		digester.addObjectCreate("circuit/connection", ConnectionDto.class);
		digester.addObjectCreate("circuit/connection/startLead", LeadDto.class);
		digester.addBeanPropertySetter("circuit/connection/startLead/name", "name");
		digester.addCallMethod("circuit/connection/startLead/type", "setType", 1);
		digester.addCallParam("circuit/connection/startLead/type", 0);
		digester.addBeanPropertySetter("circuit/connection/startLead/index", "index");
		digester.addSetNext("circuit/connection/startLead", "setStartLead");
		digester.addObjectCreate("circuit/connection/endLead", LeadDto.class);
		digester.addBeanPropertySetter("circuit/connection/endLead/name", "name");
		digester.addCallMethod("circuit/connection/endLead/type", "setType", 1);
		digester.addCallParam("circuit/connection/endLead/type", 0);
		digester.addBeanPropertySetter("circuit/connection/endLead/index", "index");
		digester.addSetNext("circuit/connection/endLead", "setEndLead");
		digester.addSetNext("circuit/connection", "addConnection");
		digestedCircuit = (CircuitDto) digester.parse(xml);
		return digestedCircuit;
	}

	private void deleteCircuit() {
		System.out.print("Ingrese el nombre del Circuito a eliminar: ");
		if (confirmOperation()) {
			System.out.print("Ingrese el nombre del Componente a eliminar: ");
			String name = readLine();
			try {
				circuitService.removeRelatedCircuits(name);
				circuitService.removeCircuit(name);
				writeLine("El circuito " + name + " ha sido eliminado.");
			} catch (CircuitElementNotFoundException e) {
				writeLine("No se encontro ningún circuito con el nombre indicado:\n\t" + e.getMessage());
			} catch (InvalidCircuitElementException e) {
				writeLine("Hubo un error al intentar modificar el circuito:\n\t" + e.getMessage());
			}
		}
	}

	private Boolean confirmOperation() {
		writeLine("Se borrarán todos los circuitos que utilicen directa o indirectamente al elemento a modificar/borrar.\n¿Desea confirmar la operación? (S/N)");
		char option = readOption();
		while (option != 's' && option != 'S' && option != 'n' && option != 'N') {
			writeLine("Por favor confirme con S o N...");
			option = readOption();
		}
		return (option != 's' || option != 'S');
	}

}
